import { ApiErrorInfo } from "@/shared/types";
import { parse as parseContentType } from "content-type";

type ResultOk<T> = {
  ok: true;
  data: T;
};

type FailureType = "BAD_REQUEST" | "TIMEOUT" | "UNAUTHORIZED" | "UNHANDLED" | "UNKNOWN";

type ResultFailed<T> = {
  ok: false;
  message: string;
  status: number;
  type: FailureType;
};

type EasyFetchResult<T> = ResultOk<T> | ResultFailed<T>;

const TIMEOUT = 1000 * 5; // 5s

type Fetch0Options = { timeout: number; method: "GET" | "POST" | "PUT"; body?: BodyInit };

/**
 * @see https://dmitripavlutin.com/timeout-fetch-request/
 */
async function fetch0(url: string, options: Fetch0Options): Promise<Response> {
  const { timeout, method, body } = options;
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  try {
    return await fetch(url, { method, body, signal: controller.signal });
  } finally {
    clearTimeout(id);
  }
}

async function handleResponse<T>(resp: Response): Promise<EasyFetchResult<T>> {
  const { status } = resp;
  if (status == 200) {
    const data: T = await resp.json();
    return { ok: true, data };
  } else {
    // prettier-ignore
    const type: FailureType = status == 400 ? "BAD_REQUEST"
                            : status == 401 ? "UNAUTHORIZED"
                            : "UNHANDLED";

    const { type: contentType } = parseContentType(resp.headers.get("Content-Type") || "");
    switch (contentType) {
      case "application/json": {
        try {
          const err: ApiErrorInfo = await resp.json();
          return { ok: false, type, status, message: err.message };
        } catch (err) {
          console.error("Unhandled fetch json error", resp.url, err);
          return { ok: false, type, status, message: "Unknown Error!" };
        }
      }
      case "text/html": {
        console.error("Unhandled fetch text/html error", resp.url, await resp.text());
        return { ok: false, type, status, message: "Unknown Error!" };
      }
      default: {
        console.error("Unhandled fetch error", resp.url);
        return { ok: false, type, status, message: "Unknown Error!" };
      }
    }
  }
}

function handleError<T>(err: unknown): EasyFetchResult<T> {
  if (!err) {
    return { ok: false, message: "Unknown error!", type: "UNKNOWN", status: 99999 };
  }

  console.error("fetch error", err);

  if (err instanceof DOMException) {
    return ["AbortError", "TimeoutError"].includes(err.name)
      ? { ok: false, status: 99999, type: "TIMEOUT", message: "Timeout!" }
      : { ok: false, status: 99999, type: "UNHANDLED", message: "Unhandled DOMException!" };
  }

  return { ok: false, type: "UNHANDLED", message: "TODO: fetch 搞个信息出来", status: 99999 };
}

async function fetch1<T>(url: string, options: Fetch0Options): Promise<EasyFetchResult<T>> {
  try {
    const resp = await fetch0(url, options);
    return await handleResponse(resp);
  } catch (err) {
    return handleError(err);
  }
}

async function get<T>(url: string): Promise<EasyFetchResult<T>> {
  return await fetch1(url, { timeout: TIMEOUT, method: "GET" });
}

async function post<T>(url: string, options: { body?: unknown }): Promise<EasyFetchResult<T>> {
  const { body } = options;
  return await fetch1(url, { timeout: TIMEOUT, method: "POST", body: JSON.stringify(body) });
}

async function put<T>(url: string, options: { body?: unknown }): Promise<EasyFetchResult<T>> {
  const { body } = options;
  return await fetch1(url, { timeout: TIMEOUT, method: "PUT", body: JSON.stringify(body) });
}

const easyFetch = { get, post, put };

export default easyFetch;
